<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/custom-elements-helpers.js"></script>
<body>
<script>
'use strict';

// Looks up the preceeding element (which should be a template
// element) and creates a Promise test. The test name is taken from
// the template's data-test attribute.
//
// The content of the template is loaded into an iframe. On load, f
// is passed the frame's content window to run assertions.
function test_with_content(f) {
  var t = document.currentScript.previousElementSibling;
  // Under the foreground parser, some elements are now created
  // interwined (like <template><iframe><template><iframe>. This means
  // the test can sometimes pick up the previous iframe, rather than
  // the template expected.
  while (t) {
    if (t.nodeName == 'TEMPLATE') {
      test_with_window(f, t.dataset.test, t.innerHTML);
      return;
    }
    t = t.previousElementSibling;
  }
  // Something's gone really wrong with the tree construction if we
  // reach here.
  assert_unreached();
}

// Searches the document for an iframe with the specified content window.
function findFrameWithWindow(w) {
  return Array.prototype.find.call(document.querySelectorAll('iframe'), (f) => {
    return f.contentWindow === w;
  });
}

test_with_window((w) => {
  assert_equals(findFrameWithWindow(w).contentWindow, w,
                'should find the frame with this window');
  assert_equals(findFrameWithWindow(window), undefined,
                'should return undefined if there is no such frame');
}, 'sanity check the findFrameWithWindow function');
</script>

<template data-test="the parser synchronously creates elements">
  <script>
  'use strict';

  window.invocations = [];
  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
      invocations.push('constructor');
    }
    static get observedAttributes() { return ['x']; }
    attributeChangedCallback(name, oldValue, newValue, nsuri) {
      invocations.push(`${name}="${newValue}"`);
    }
    connectedCallback() {
      invocations.push('connected');
    }
  });
  </script>
  <a-a x="y">
    <script>
    'use strict';

    invocations.push('script');
    </script>
  </a-a>
</template>
<script>
'use strict';

test_with_content((w) => {
  assert_array_equals(w.invocations,
                      ['constructor', 'x="y"', 'connected', 'script']);
});
</script>

<template data-test="foreign content insertion executes connected">
  <script>
  'use strict';

  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
    }
    connectedCallback() {
      window.connectedChildNodeCount = this.childNodes.length;
    }
  });
  </script>
  <a-a><div></div></a-a>
</template>
<script>
'use strict';

test_with_content((w) => {
  assert_equals(w.connectedChildNodeCount, 0,
      'the parser should have run the connected callback when inserting the ' +
      'element, before continuing tree construction');
});
</script>

<template data-test="element creation failure produces unknown element">
  <script>
  'use strict';

  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();

      // Returning this different, in-use element causes element
      // creation to fail in
      // https://dom.spec.whatwg.org/#concept-create-element steps
      // 6.4-9, eg: "If result has children then then throw a
      // NotSupportedError."
      return document.documentElement;
    }
  });
  </script>
  <a-a>
</template>
<script>
'use strict';

test_with_content((w) => {
  let e = w.document.querySelector('a-a');
  assert_true(e.matches(':not(:defined)'));
  assert_equals(Object.getPrototypeOf(e), w.HTMLUnknownElement.prototype);
});
</script>

<template data-test="modify tree during creation">
  <script>
  'use strict';

  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
      document.querySelector('b').remove();
    }
  });
  </script>
  <b>
    <a-a>
  </b>
</template>
<script>
'use strict';

test_with_content((w) => {
  assert_equals(w.document.querySelectorAll('b').length, 0);
});
</script>

<template data-test="destructive writes are blocked during construction">
  <script>
  // Custom element constructors do not set the insertion point, which
  // makes document.write() "destructive." However they increment the
  // throw-on-dynamic-insertion counter, which blocks document.write and
  // throws "InvalidStateError" DOMException.
  // https://html.spec.whatwg.org/#create-an-element-for-the-token
  // https://html.spec.whatwg.org/#document.write()
  'use strict';

  window.errors = [];
  window.onerror = function (event, source, lineno, colno, error) {
    errors.push(error.name);
    return true; // Cancel the error event.
  };

  window.invocations = [];
  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
      invocations.push('constructor');
      document.write(
          `<script>'use strict'; invocations.push('written');</scr${'i'}pt>`);
    }
    connectedCallback() {
      invocations.push('connected');
    }
  });
  </script>
  <a-a>
  <script>
  'use strict';
  invocations.push('parsed');
  </script>
</template>
<script>
'use strict';

test_with_content((w) => {
  assert_array_equals(
      w.invocations,
      ['constructor', 'parsed'],
      'the destructive document.write content should have been ignored; ' +
      'connectedCallback should not be executed, since the constructor threw');
  assert_array_equals(
      w.errors,
      ['InvalidStateError'],
      'create an element for the token should report the exception');
});
</script>

<template data-test="non-destructive writes are not blocked">
  <script>
  // Script running sets the insertion point, which makes makes
  // document.write() "non-destructive." Custom elements should block
  // non-destructive writes.
  // https://html.spec.whatwg.org/#create-an-element-for-the-token
  // https://html.spec.whatwg.org/#document.write()
  'use strict';

  window.errors = [];
  window.onerror = function (event, source, lineno, colno, error) {
    errors.push(error.name);
    return true; // Cancel the error event.
  };

  window.invocations = [];
  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
      invocations.push('constructor');
      document.write(
          `<script>'use strict'; invocations.push('written');</scr${'i'}pt>`);
    }
    connectedCallback() {
      invocations.push('connected');
    }
  });
  document.write('<a-a>');
  invocations.push('post write');
  </script>
  <script>
  'use strict';
  invocations.push('parsed');
  </script>
</template>
<script>
'use strict';

test_with_content((w) => {
  assert_array_equals(
      w.invocations,
      ['constructor', 'post write', 'parsed'],
      'the non-destructive document.write content should have been ignored' +
      'connectedCallback should not be executed, since the constructor threw');
  assert_array_equals(
      w.errors,
      ['InvalidStateError'],
      'create an element for the token should report the exception');
});
</script>

<template data-test="innerHTML is not blocked by custom element constructors">
  <script>
  'use strict';

  window.invocations = [];
  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
      invocations.push(`construct ${this.id}`);
      if (!this.id) {
        // If the ID attribute is not set, this was created
        // synchronously by the parser. Adding children at this point
        // would cause creation to fail, so embiggen the previous
        // element instead.
        document.querySelector('span').innerHTML = `<a-a id="r">`;
      }
    }
    connectedCallback() {
      invocations.push(`connected ${this.parentNode.localName}/${this.id}`);
    }
  });
  </script>
  <span></span>
  <a-a id="q"></a-a>
  <script>
  'use strict';
  invocations.push('parsed');
  </script>
</template>
<script>
'use strict';

test_with_content((w) => {
  assert_array_equals(
      w.invocations,
      ['construct ', 'construct r', 'connected span/r', 'connected body/q',
       'parsed'],
      'custom element constructors should not block innerHTML');
});
</script>


<template data-test="parsing without a browsing context should not create custom elements">
  <body>
  <script>
  'use strict';

  let f = parent.findFrameWithWindow(window);
  f.invocations = [];

  customElements.define('a-a', class extends HTMLElement {
    constructor() {
      super();
      f.invocations.push(this);
    }
  });
  </script>
  <a-a></a-a>
  <script>
  f.detached = document.implementation.createHTMLDocument();
  f.detached.documentElement.appendChild(document.body);
  </script>
  <a-a></a-a>
</template>
<script>
'use strict';

test_with_content((w) => {
  let f = findFrameWithWindow(w);
  assert_array_equals(f.invocations,
                      [f.detached.querySelector('a-a:first-of-type')],
                      'one element should have been constructed');
  assert_true(f.invocations[0].matches(':defined'),
              'the element should have been created successfully');

  let elements = f.detached.querySelectorAll('a-a');
  console.log(f.invocations[0].parentNode);
  assert_equals(elements.length, 2,
                'two elements should have been created');
  assert_equals(Object.getPrototypeOf(elements[1]), w.HTMLElement.prototype,
                'the second element should be un-upgraded, not failed');
});
</script>
